package com.moralesce.gt.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import com.moralesce.gt.math.Combination;
import com.moralesce.gt.math.Enumerations;
import com.moralesce.gt.model.Board;
import com.moralesce.gt.model.Card;
import com.moralesce.gt.model.Deck;
import com.moralesce.gt.model.Hand;
import com.moralesce.gt.model.HandType;
import com.moralesce.gt.model.Rank;
import com.moralesce.gt.model.StartingHand;

public final class HandUtil {

	private static final FastLogger log = FastLogger.newInstance(HandUtil.class);

	private HandUtil() {

	}

	/**
	 * Groups the specified hands by hand type.
	 * 
	 * <p>
	 * This method groups the specified list of hands by hand type. The result
	 * produces an ordered map, with the hand type as the key and a list of
	 * hands with the given hand type as the value.
	 * </p>
	 * 
	 * <p>
	 * The map is guaranteed have exactly nine entries, one entry for each hand
	 * type. If there are no hands for a particular hand type, the entry value
	 * will contain an empty list.
	 * </p>
	 * 
	 * @param hands The hands to group.
	 * 
	 * @return A SortedMap<HandType, List<Hand>> with all hands in the specified
	 * list grouped by hand type.
	 */
	public static SortedMap<HandType, List<Hand>> groupHandsByType(List<Hand> hands) {
		SortedMap<HandType, List<Hand>> values = new TreeMap<HandType, List<Hand>>();
		if (hands == null || hands.isEmpty()) {
			return values;
		}
		for (HandType handType : HandType.values()) {
			values.put(handType, new ArrayList<Hand>());
		}
		for (Hand hand : hands) {
			values.get(hand.type()).add(hand);
		}
		return values;
	}

	/**
	 * Returns a map of the strongest hands that have been made or could be made
	 * with the given starting hand and board, grouped by hand type.
	 * 
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return A SortedMap<HandType, List<Hand>> of the strongest hands that
	 * have either been made or could be made with the given starting hand and
	 * board.
	 */
	public static SortedMap<HandType, List<Hand>> strongestHands(StartingHand startingHand, Board board) {
		List<Hand> hands = filterHands(startingHand, board);

		SortedMap<HandType, List<Hand>> handsByType = new TreeMap<HandType, List<Hand>>();
		for (HandType handType : HandType.values()) {
			handsByType.put(handType, new ArrayList<Hand>());
		}

		Map<String, Hand> handsByName = new HashMap<String, Hand>();
		for (Hand hand : hands) {
			handsByName.put(hand.name(), hand);
		}

		for (Map.Entry<String, Hand> entry : handsByName.entrySet()) {
			handsByType.get(entry.getValue().type()).add(entry.getValue());
		}

		return handsByType;
	}

	/**
	 * Returns a map containing the probability of drawing each type of hand,
	 * given the specified starting hand and board.
	 * 
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return A SortedMap<HandType, Double> containing the probability of
	 * drawing each hand type, given the specified starting hand and board.
	 */
	public static SortedMap<HandType, Double> probabilitiesByHandType(StartingHand startingHand, Board board) {
		SortedMap<HandType, Double> probabilities = new TreeMap<HandType, Double>();
		for (HandType handType : HandType.values()) {
			probabilities.put(handType, probabilityOfHandType(handType, startingHand, board));
		}
		return probabilities;
	}

	/**
	 * Returns the probability of drawing a specifed hand type given the
	 * specified starting hand and board.
	 * 
	 * @param type The hand type.
	 * @param startingHand The starting hand.
	 * @param board The board.
	 * 
	 * @return The probability (from 0 to 100) of drawing a specified hand type
	 * given the specified starting hand and board
	 */
	public static Double probabilityOfHandType(HandType type, StartingHand startingHand, Board board) {
		if (board.cards().size() < 3) {
			throw new IllegalStateException("Can only calculate probabilities after the flop.");
		}
		log.debug("Calculating probability for " + type.label().toLowerCase());

		Deck deck = new Deck().remove(ListUtil.merge(startingHand.cards(), board.cards()));
		List<List<Card>> draws = new Combination<Card>(deck.cards(), (5 - board.cards().size())).list();

		int total = 0;
		for (List<Card> cards : draws) {
			List<Hand> hands = Enumerations.allHands(startingHand, Board.newInstance(ListUtil.merge(board.cards(), cards)));
			for (Hand hand : hands) {
				if (hand.type().equals(type)) {
					total += 1;
					break;
				}
			}
		}
		log.debug(draws.size() + " hand combinations found, of which " + total + " are " + type.label().toLowerCase());

		return ((double) total / draws.size()) * 100.00;
	}

	private static List<Hand> filterHands(StartingHand startingHand, Board board) {
		List<Hand> hands = Enumerations.distinctHands(startingHand, board);
		List<Hand> strategicHands = new ArrayList<Hand>();

		for (Hand hand : hands) {
			if (hand.hasHighCard()) {
				continue;
			}
			if (hand.hasPair()) {
				if (hasPair(communityCards(startingHand, hand))) {
					continue;
				}
			}
			if (hand.hasTwoPair()) {
				if (hasPair(communityCards(startingHand, hand))) {
					continue;
				}
			}
			if (hand.hasThreeOfAKind()) {
				if (hasThreeOfAKind(communityCards(startingHand, hand))) {
					continue;
				}
			}
			strategicHands.add(hand);
		}

		return ListUtil.sort(strategicHands);
	}

	private static List<Card> communityCards(StartingHand startingHand, Hand hand) {
		List<Card> cards = ListUtil.copy(hand.cards());
		cards.removeAll(startingHand.cards());
		return cards;
	}

	private static boolean hasThreeOfAKind(List<Card> cards) {
		for (Rank rank : Rank.values()) {
			if (count(rank, cards) == 3) {
				return true;
			}
		}
		return false;
	}

	private static boolean hasPair(List<Card> cards) {
		for (Rank rank : Rank.values()) {
			if (count(rank, cards) == 2) {
				return true;
			}
		}
		return false;
	}

	private static Integer count(Rank rank, List<Card> cards) {
		int total = 0;
		for (Card card : cards) {
			if (card.rank().equals(rank)) {
				total += 1;
			}
		}
		return total;
	}

}
